//---------------------------------------------------------------------
// Authors: jachymko
//
// Description: Class which dynamically compiles binary record parsers.
//
// Creation Date: Dec 24, 2006
//---------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;

namespace Pscx.Runtime.Serialization.Binary {
    
    public class RecordParserBuilder {

        readonly Type recordType;
        readonly ConstructorInfo recordCtor;
        
        readonly Type parserType;

        readonly int pack = 0;

        DynamicMethod method;
        ILGenerator il;

        LocalBuilder retval = null;
        FieldInfo field = null;
        Type fieldType = null;

        protected internal RecordParserBuilder(BinaryParser parser, Type recordType) {
            this.recordType = recordType;
            this.recordCtor = recordType.GetConstructor(new Type[0]);
            this.parserType = parser.GetType();

            BinaryRecordAttribute attr = BinaryParser.GetAttribute<BinaryRecordAttribute>(recordType);

            Throw.IfIsNull(recordCtor, "type {0} does not have default constructor", recordType);
            Throw.IfIsNull(attr, "type {0} does not have BinaryRecordAttribute", recordType);

            this.pack = attr.Pack;

        }

        internal DynamicMethod Build() {

            if (method != null) return method;

            DynamicMethod meth = new DynamicMethod(
                MethodName,
                recordType,
                new Type[] { parserType },
                parserType,
                true);

            meth.DefineParameter(1, ParameterAttributes.In, "parser");

            il = meth.GetILGenerator();

            foreach (FieldInfo f in Fields) {
                this.field = f;
                this.fieldType = field.FieldType;

                if (fieldType.IsEnum) {
                    this.fieldType = Enum.GetUnderlyingType(fieldType);
                }

                EmitReadCall();
                EmitVerifyMagic();
                EmitAlign();
            }


            il.Emit(OpCodes.Ldloc, ReturnLocal);
            il.Emit(OpCodes.Ret);

            il = null;
            field = null;
            fieldType = null;

            return method = meth;
        }

        protected virtual void EmitRead(ILGenerator il, FieldInfo field) {
            if (IsRecord(fieldType)) {
                EmitReadRecord();
            }

            else if (fieldType == typeof(string)) {
                EmitReadString();
            }

            else if (fieldType == typeof(DateTime)) {
                EmitReadDateTime();
            }

            else if (fieldType == typeof(Version)) {
                EmitReadVersion();
            }

            else {
                EmitReadPrimitive();
            }
        }

        void EmitAlign() {
            if (pack > 1) {
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldc_I4, pack);
                il.EmitCall(OpCodes.Callvirt, miAlign, null);
            }
        }
        
        void EmitVerifyMagic() {
            MagicSignatureAttribute magic = BinaryParser.GetAttribute<MagicSignatureAttribute>(field);
            if (magic == null) return;

            #region |     T expected = magic.Signature;

            LocalBuilder expected = il.DeclareLocal(field.FieldType);
            EmitLoadAnyConstInt(il, Type.GetTypeCode(field.FieldType), magic.Signature);
            il.Emit(OpCodes.Stloc, expected);
            il.Emit(OpCodes.Ldloc, expected);

            #endregion
            #region |     T actual = retval.Field

            EmitLoadReturnField(field);

            #endregion
            #region |     if(actual != expected) {

            Label continueBranch = il.DefineLabel();
            il.Emit(OpCodes.Beq_S, continueBranch);

            #endregion
            #region |          BinaryParser.ThrowSignatureException(expected, actual);
            
            il.EmitCall(OpCodes.Call, miThrowSignatureException, null);

            #endregion
            #region |    }

            il.MarkLabel(continueBranch);

            #endregion

        }

        void EmitLoadAnyConstInt(ILGenerator il, TypeCode tp, object n) {
            switch (tp) {
                case TypeCode.SByte: il.Emit(OpCodes.Ldc_I4_S, Convert.ToSByte(n)); break;
                case TypeCode.Int16: il.Emit(OpCodes.Ldc_I4, Convert.ToInt16(n)); break;
                case TypeCode.Int32: il.Emit(OpCodes.Ldc_I4, Convert.ToInt32(n)); break;
                case TypeCode.Int64: il.Emit(OpCodes.Ldc_I8, Convert.ToInt64(n)); break;

                case TypeCode.Byte: il.Emit(OpCodes.Ldc_I4_S, Convert.ToByte(n)); break;
                case TypeCode.UInt16: il.Emit(OpCodes.Ldc_I4, Convert.ToUInt16(n)); break;
                case TypeCode.UInt32: il.Emit(OpCodes.Ldc_I4, Convert.ToUInt32(n)); break;
                case TypeCode.UInt64: il.Emit(OpCodes.Ldc_I4, Convert.ToUInt64(n)); break;

                default:
                    Throw.ArgumentOutOfRangeException("Dont know how to emit constant with typecode {0}", tp.ToString());
                    break;
            }
        }

        void EmitReadCall() {

            il.Emit(OpCodes.Ldloc, ReturnLocal);

            EmitRead(il, field);

            il.Emit(OpCodes.Stfld, field);
        }

        void EmitReadDateTime() {
            BinaryFieldAttribute attr = BinaryParser.GetAttribute<BinaryFieldAttribute>(field);

            if (attr != null) {
                switch (attr.FieldType) {
                    case BinaryFieldType.UnixTime:

                        EmitParserCallNoArgs(miReadInt32);
                        il.EmitCall(OpCodes.Callvirt, miUnixToDateTime, null);

                        return;
                }
            }

            Throw.ArgumentException("Don't know how to parse DateTime field " + field.ToString());
        }

        void EmitReadPrimitive() {
            MethodInfo read = SelectReadPrimitive(Type.GetTypeCode(fieldType));

            if (read != null) {
                EmitParserCallNoArgs(read);
            }
            else {
                BinaryParserException.ThrowUnknownType(fieldType);
            }
        }

        void EmitReadRecord() {
            Throw.ArgumentExceptionIf((fieldType == field.DeclaringType), "Parsing field {0} would loop forever.", field);

            EmitParserCallNoArgs(miReadRecord.MakeGenericMethod(fieldType));
        }

        void EmitReadString() {
            BinaryFieldAttribute attr = BinaryParser.GetAttribute<BinaryFieldAttribute>(field);

            if (attr != null) {
                switch (attr.FieldType) {
                    case BinaryFieldType.StringAsciiZ:
                        if (attr.FixedLength == 0) {
                            EmitParserCallNoArgs(miReadStringAsciiZ);
                        }
                        else {
                            il.Emit(OpCodes.Ldarg_0);
                            il.Emit(OpCodes.Ldc_I4, attr.FixedLength);
                            il.Emit(OpCodes.Callvirt, miReadStringAsciiZFixed);
                        }
                        return;
                }
            }

            Throw.ArgumentException(field.Name, "Don't know how to parse field " + field.ToString());
        }

        void EmitReadVersion() {
            VersionFieldAttribute attr = BinaryParser.GetAttribute<VersionFieldAttribute>(field);

            Type[] ctorParams = new Type[attr.ComponentCount];
            MethodInfo read = SelectReadPrimitive(attr.ComponentType);

            for (int i = 0; i < attr.ComponentCount; i++) {
                EmitParserCallNoArgs(read);
                ctorParams[i] = typeof(int);
            }

            ConstructorInfo ctor = typeof(Version).GetConstructor(ctorParams);
            il.Emit(OpCodes.Newobj, ctor);
        }

        void EmitLoadReturnField(FieldInfo field) {
            il.Emit(OpCodes.Ldloc, ReturnLocal);
            il.Emit(OpCodes.Ldfld, field);
        }

        void EmitParserCallNoArgs(MethodInfo meth) {
            il.Emit(OpCodes.Ldarg_0);
            il.EmitCall(OpCodes.Callvirt, meth, null);
        }

        string MethodName {
            get { return "Read$" + recordType.FullName.Replace('.', '$'); }
        }

        LocalBuilder ReturnLocal {
            get {
                if (retval == null) {
                    this.retval = il.DeclareLocal(recordType);

                    il.Emit(OpCodes.Newobj, recordCtor);
                    il.Emit(OpCodes.Stloc, retval);
                }

                return retval;
            }
        }

        IEnumerable<FieldInfo> Fields {
            get {
                FieldInfo[] fis = recordType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

                foreach (FieldInfo fi in fis) {
                    if (!IsNonSerialized(fi)) yield return fi;
                }
            }
        }

        static bool IsRecord(Type t) {
            return BinaryParser.GetAttribute<BinaryRecordAttribute>(t) != null;
        }

        static bool IsNonSerialized(MemberInfo mi) {
            return BinaryParser.GetAttribute<NonSerializedAttribute>(mi) != null;
        }

        static MethodInfo SelectReadPrimitive(TypeCode typeCode) {
            switch (typeCode) {
                case TypeCode.Byte: return miReadByte;
                case TypeCode.UInt16: return miReadUInt16;
                case TypeCode.UInt32: return miReadUInt32;
                case TypeCode.UInt64: return miReadUInt64;

                case TypeCode.SByte: return miReadSByte;
                case TypeCode.Int16: return miReadInt16;
                case TypeCode.Int32: return miReadInt32;
                case TypeCode.Int64: return miReadInt64;

                default: return null;
            }
        }

        static RecordParserBuilder() {
            foreach (MethodInfo mi in typeof(BinaryParser).GetMethods()) {
                if (mi.IsGenericMethodDefinition && mi.Name == "Read") {
                    miReadRecord = mi;
                    break;
                }
            }
        }

        static readonly BindingFlags publicStatic = BindingFlags.Public | BindingFlags.Static;
        static readonly BindingFlags nonpublicStatic = BindingFlags.NonPublic | BindingFlags.Static;

        static readonly MethodInfo miAlign = typeof(BinaryParser).GetMethod("Align");

        static readonly MethodInfo miReadByte = typeof(BinaryParser).GetMethod("ReadByte");
        static readonly MethodInfo miReadUInt16 = typeof(BinaryParser).GetMethod("ReadUInt16");
        static readonly MethodInfo miReadUInt32 = typeof(BinaryParser).GetMethod("ReadUInt32");
        static readonly MethodInfo miReadUInt64 = typeof(BinaryParser).GetMethod("ReadUInt64");

        static readonly MethodInfo miReadSByte = typeof(BinaryParser).GetMethod("ReadSByte");
        static readonly MethodInfo miReadInt16 = typeof(BinaryParser).GetMethod("ReadInt16");
        static readonly MethodInfo miReadInt32 = typeof(BinaryParser).GetMethod("ReadInt32");
        static readonly MethodInfo miReadInt64 = typeof(BinaryParser).GetMethod("ReadInt64");

        static readonly MethodInfo miReadRecord = typeof(BinaryParser).GetMethod("ReadRecord");
        static readonly MethodInfo miReadStringAsciiZ = typeof(BinaryParser).GetMethod("ReadStringAsciiZ", new Type[0]);
        static readonly MethodInfo miReadStringAsciiZFixed = typeof(BinaryParser).GetMethod("ReadStringAsciiZ", new Type[] { typeof(int) });

        static readonly MethodInfo miUnixToDateTime = typeof(UnixTime).GetMethod("ToDateTime", publicStatic);
        static readonly MethodInfo miThrowSignatureException =
            typeof(BinaryParserException).GetMethod("ThrowSignatureException", nonpublicStatic);

        
    }
}
